using System;
using System.Linq;
using System.ComponentModel;
using System.Collections.Generic;
using System.Runtime.Serialization;

using SharpGL.Media3D;
using SharpGL.Enumerations;

namespace SharpGL.SceneGraph
{
    [DataContract()]
    public class BoundingVolume : INotifyPropertyChanged, IRenderable
	{
        #region Private Fields

        [DataMember()]
        private Rect3D boundingRect = Rect3D.Empty;
        
        #endregion Private Fields

        #region Protected Methods

        /// <summary>
        /// Call this function to do the PropertyChanged event.
        /// </summary>
        protected void NotifyPropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion Protected Methods

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="BoundingVolume"/> class.
        /// </summary>
        public BoundingVolume() 
		{
		}

        /// <summary>
        /// Initializes a new instance of the <see cref="BoundingVolume"/> class.
        /// </summary>
        public BoundingVolume(Rect3D boundingRect)
        {
            this.boundingRect = boundingRect;
        }

        #endregion Constructors

        #region Public Methods

        /// <summary>
        /// Render to the provided instance of OpenGL.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        /// <param name="renderMode">The render mode.</param>
        public void Render(OpenGL gl, RenderMode renderMode)
        {
            //  Push attributes, disable lighting.
            gl.PushAttrib(OpenGL.GL_CURRENT_BIT | OpenGL.GL_ENABLE_BIT |
                OpenGL.GL_LINE_BIT | OpenGL.GL_POLYGON_BIT);
            gl.Disable(OpenGL.GL_LIGHTING);
            gl.Disable(OpenGL.GL_TEXTURE_2D);
            gl.LineWidth(1.0f);
            gl.Color(1f, 0.2f, 0.2f, 0.6f);
            gl.PolygonMode(OpenGL.GL_FRONT_AND_BACK,
                renderMode == RenderMode.HitTest ? (uint)PolygonMode.Filled : (uint)PolygonMode.Lines);

            // Create bounding points of volume.
            Point3D lll = MinPoint;
            Point3D hhh = MaxPoint;

            gl.Begin(OpenGL.GL_QUADS);		// Draw The Cube Using quads
            gl.Vertex(hhh.X, hhh.Y, lll.Z);	// Top Right Of The Quad (Top)
            gl.Vertex(lll.X, hhh.Y, lll.Z);	// Top Left Of The Quad (Top)
            gl.Vertex(lll.X, hhh.Y, hhh.Z);	// Bottom Left Of The Quad (Top)
            gl.Vertex(hhh.X, hhh.Y, hhh.Z);	// Bottom Right Of The Quad (Top)
            gl.Vertex(hhh.X, lll.Y, hhh.Z);	// Top Right Of The Quad (Bottom)
            gl.Vertex(lll.X, lll.Y, hhh.Z);	// Top Left Of The Quad (Bottom)
            gl.Vertex(lll.X, lll.Y, lll.Z);	// Bottom Left Of The Quad (Bottom)
            gl.Vertex(hhh.X, lll.Y, lll.Z);	// Bottom Right Of The Quad (Bottom)
            gl.Vertex(hhh.X, hhh.Y, hhh.Z);	// Top Right Of The Quad (Front)
            gl.Vertex(lll.X, hhh.Y, hhh.Z);	// Top Left Of The Quad (Front)
            gl.Vertex(lll.X, lll.Y, hhh.Z);	// Bottom Left Of The Quad (Front)
            gl.Vertex(hhh.X, lll.Y, hhh.Z);	// Bottom Right Of The Quad (Front)
            gl.Vertex(hhh.X, lll.Y, lll.Z);	// Top Right Of The Quad (Back)
            gl.Vertex(lll.X, lll.Y, lll.Z);	// Top Left Of The Quad (Back)
            gl.Vertex(lll.X, hhh.Y, lll.Z);	// Bottom Left Of The Quad (Back)
            gl.Vertex(hhh.X, hhh.Y, lll.Z);	// Bottom Right Of The Quad (Back)
            gl.Vertex(lll.X, hhh.Y, hhh.Z);	// Top Right Of The Quad (Left)
            gl.Vertex(lll.X, hhh.Y, lll.Z);	// Top Left Of The Quad (Left)
            gl.Vertex(lll.X, lll.Y, lll.Z);	// Bottom Left Of The Quad (Left)
            gl.Vertex(lll.X, lll.Y, hhh.Z);	// Bottom Right Of The Quad (Left)
            gl.Vertex(hhh.X, hhh.Y, lll.Z);	// Top Right Of The Quad (Right)
            gl.Vertex(hhh.X, hhh.Y, hhh.Z);	// Top Left Of The Quad (Right)
            gl.Vertex(hhh.X, lll.Y, hhh.Z);	// Bottom Left Of The Quad (Right)
            gl.Vertex(hhh.X, lll.Y, lll.Z);	// Bottom Right Of The Quad (Right)
            gl.End();			// End Drawing The Cube

            //  Pop attributes.
            gl.PopAttrib();
        }

        /// <summary>
        /// Creates the volume from vertices.
        /// </summary>
        /// <param name="vertices">The vertices.</param>
        public void FromVertices(IEnumerable<Point3D> vertices)
        {
            var vertexList = vertices.ToList();
            
            if (vertexList.Count > 0)
            {
                Point3D lll = vertexList[0];
                Point3D hhh = vertexList[0];

                for (int i = 1; i < vertexList.Count; i++)
                {
                    lll.X = Math.Min(lll.X, vertexList[i].X);
                    lll.Y = Math.Min(lll.Y, vertexList[i].Y);
                    lll.Z = Math.Min(lll.Z, vertexList[i].Z);

                    hhh.X = Math.Max(hhh.X, vertexList[i].X);
                    hhh.Y = Math.Max(hhh.Y, vertexList[i].Y);
                    hhh.Z = Math.Max(hhh.Z, vertexList[i].Z);
                }

                boundingRect = new Rect3D(lll, hhh - lll);
            }
            else
            {
                boundingRect = Rect3D.Empty;
            }
        }

        /// <summary>
        /// Creates the volume from a spherical volume.
        /// </summary>
        /// <param name="centrer">The centre.</param>
        /// <param name="radius">The radius.</param>
        public void FromSphericalVolume(Point3D centre, float radius)
        {
            //  Set the centre.
            boundingRect = new Rect3D(centre, new Size3D(0.0f, 0.0f, 0.0f));

            //  Pad by the radius.
            Pad(radius);
        }

        /// <summary>
        /// Creates the volume from a cylindrical volume.
        /// </summary>
        /// <param name="baseline">The baseline.</param>
        /// <param name="height">The height.</param>
        /// <param name="baseRadius">The base radius.</param>
        /// <param name="topRadius">The top radius.</param>
        public void FromCylindricalVolume(Point3D baseline, float height, float baseRadius, float topRadius)
        {
            Point3D[] set = new Point3D[6];

            set[0] = baseline;
            set[1] = baseline + new Vector3D(0.0f, 0.0f, height);

            set[2] = baseline + new Vector3D(baseRadius, baseRadius, 0.0f);
            set[3] = baseline + new Vector3D(-baseRadius, -baseRadius, 0.0f);

            set[4] = set[1] + new Vector3D(topRadius, topRadius, 0.0f);
            set[5] = set[1] + new Vector3D(-topRadius, -topRadius, 0.0f);

            FromVertices(set);            
        }

        /// <summary>
        /// Pads the bounding volume.
        /// </summary>
        /// <param name="padding">The padding.</param>
        public void Pad(float padding)
        {
            boundingRect.X -= padding;
            boundingRect.Y -= padding;
            boundingRect.Z -= padding;

            padding *= 2.0f;
            boundingRect.SizeX += padding;
            boundingRect.SizeY += padding;
            boundingRect.SizeZ += padding;
        }

        /// <summary>
        /// Set zero for all values.
        /// </summary>
        public void SetEmpty()
        {
            boundingRect = Rect3D.Empty;
        }

        /// <summary>
        /// Transform the bounding volume.
        /// </summary>
        /// <param name="transformationMatrix">The transformation matrix [4, 4].</param>
        public void Transform(Matrix transformMatrix)
        {
            // Check parameters.
            if ((Object)transformMatrix == null)
                return;
            
            // Create bounding points of volume.
            Point3D lll = MinPoint;
            Point3D hhh = MaxPoint;

            // Create and init eight points of bounding volume.
            Point3D[] boundingPoints = new Point3D[]
                {
                    new Point3D(lll.X, lll.Y, lll.Z),
                    new Point3D(lll.X, hhh.Y, lll.Z),
                    new Point3D(hhh.X, lll.Y, lll.Z),
                    new Point3D(hhh.X, hhh.Y, lll.Z),
                    new Point3D(lll.X, lll.Y, hhh.Z),
                    new Point3D(lll.X, hhh.Y, hhh.Z),
                    new Point3D(hhh.X, lll.Y, hhh.Z),
                    new Point3D(hhh.X, hhh.Y, hhh.Z)
                };

            // Transform every bounding point.
            for (int pointIndex = 0; pointIndex < boundingPoints.Length; pointIndex++)
                boundingPoints[pointIndex] = transformMatrix * boundingPoints[pointIndex];

            // Find dimentions of new bounding volume.
            lll = boundingPoints[0];
            hhh = boundingPoints[0];
            for (int pointIndex = 1; pointIndex < boundingPoints.Length; pointIndex++)
            {
                if (lll.X > boundingPoints[pointIndex].X)
                    lll.X = boundingPoints[pointIndex].X;
                else if (hhh.X < boundingPoints[pointIndex].X)
                    hhh.X = boundingPoints[pointIndex].X;

                if (lll.Y > boundingPoints[pointIndex].Y)
                    lll.Y = boundingPoints[pointIndex].Y;
                else if (hhh.Y < boundingPoints[pointIndex].Y)
                    hhh.Y = boundingPoints[pointIndex].Y;

                if (lll.Z > boundingPoints[pointIndex].Z)
                    lll.Z = boundingPoints[pointIndex].Z;
                else if (hhh.Z < boundingPoints[pointIndex].Z)
                    hhh.Z = boundingPoints[pointIndex].Z;
            }

            boundingRect = new Rect3D(lll, hhh - lll);
        }

        /// <summary>
        /// Join the bounding volumes.
        /// </summary>
        /// <param name="boundingVolume">The joining bounding volume.</param>
        public void Union(BoundingVolume boundingVolume)
        {
            this.boundingRect.Union(boundingVolume.boundingRect);
        }

        #endregion Public Methods

        #region Public Events

        [Description("Called when properties of BoundingVolume has been changed."), Category("SharpGL")]
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion Public Events

        #region Public Properties

        public bool IsEmpty
        {
            get { return boundingRect.IsEmpty; }
        }

        public Point3D Centre
        {
            get
            {
                return new Point3D(boundingRect.X + boundingRect.SizeX / 2.0f,
                                   boundingRect.Y + boundingRect.SizeY / 2.0f,
                                   boundingRect.Z + boundingRect.SizeZ / 2.0f);
            }
            set
            {
                BoundingRect = new Rect3D(value - new Vector3D(boundingRect.SizeX / 2.0f, boundingRect.SizeY / 2.0f, boundingRect.SizeZ / 2.0f), boundingRect.Size);
            }

        }

        public Point3D MinPoint
        {
            get { return boundingRect.Location; }
        }

        public Point3D MaxPoint
        {
            get { return boundingRect.Location + new Vector3D(boundingRect.SizeX, boundingRect.SizeY, boundingRect.SizeZ); }
        }

        /// <summary>
        /// Gets the bound dimensions.
        /// </summary>
        public Size3D Size
        {
            get { return boundingRect.Size; }
        }

        public Rect3D BoundingRect
        {
            get { return boundingRect; }
            set
            {
                if (boundingRect != value)
                {
                    boundingRect = value;

                    NotifyPropertyChanged("BoundingRect");
                }
            }
        }

        #endregion Public Properties
    }
}
